SPB Git forge

spb/ai-atlas

Public
41commits 1branches 0releases
4.6 MBsize
maindefault branch
12 days agolast push
HTML 77.2% TypeScript 10.5% Python 9.6% JavaScript 2.5%
16.8 KB · 314 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import { ScrollX } from '@/components/models/scroll-x';3import Link from 'next/link';4import { notFound, permanentRedirect } from 'next/navigation';5import { CompareTrayBar } from '@/components/compare/compare-tray-bar';6import { CompareButton } from '@/components/compare/compare-button';7import { ViewBeacon } from '@/components/layout/view-beacon';8import { OpennessChip } from '@/components/models/badges';9import { FamilyBenchmarkProgress, FamilyReleaseLanes } from '@/components/models/family-blocks';10import { MiniGraph } from '@/components/models/mini-graph';11import { Chip, EntityBadge, StatusBadge } from '@/components/ui/badges';12import { DataTable, Td, Th } from '@/components/ui/data-table';13import { EntityLink, QualityMark } from '@/components/ui/entity';14import { Hint } from '@/components/ui/hint';15import { Container, Note } from '@/components/ui/section';16import { EmptyState } from '@/components/ui/unavailable';17import { ApiError, apiD1, safe } from '@/lib/api';18import { fmtDate, fmtInt, fmtParams, fmtTokens, num } from '@/lib/format';19import { routes, SITE_NAME, SITE_URL } from '@/lib/site';20import type { FamilyDetail } from '@/lib/types';2122type Params = { params: Promise<{ slug: string }> };23export const revalidate = 600;2425async function load(slug: string): Promise<FamilyDetail> {26  try {27    return await apiD1.family(slug, 300);28  } catch (e) {29    if (e instanceof ApiError && e.notFound) notFound();30    throw e;31  }32}3334function describe(f: FamilyDetail): string {35  const pmin = num(f.param_range?.min);36  const pmax = num(f.param_range?.max);37  const bits = [`${fmtInt(f.model_count)} canonical models`, f.first_release ? `released ${fmtDate(f.first_release)} → ${f.last_release ? fmtDate(f.last_release) : 'today'}` : null, pmin !== null ? `${fmtParams(pmin)}${pmax !== null && pmax !== pmin ? ` – ${fmtParams(pmax)}` : ''} parameters` : null, f.licenses?.length ? `licences ${f.licenses.map((l) => l.key).slice(0, 3).join(', ')}` : null, num(f.artifacts_count) ? `${fmtInt(f.artifacts_count)} artifacts` : null].filter(Boolean);38  return `${f.name}${f.organization ? ` by ${f.organization.name}` : ''} — ${bits.join('; ')}. Release timeline, members, lineage, licence mix and benchmark progress on ${SITE_NAME}.`.slice(0, 300);39}4041export async function generateMetadata({ params }: Params): Promise<Metadata> {42  const { slug } = await params;43  const f = await safe(apiD1.family(slug, 5));44  if (!f || f.slug !== slug) return { title: 'Family', robots: { index: false } };45  const title = `${f.name} family — Releases, Members, Lineage & Benchmarks`;46  const description = describe(f);47  const canonical = routes.family(f.slug);48  return { title, description, alternates: { canonical }, openGraph: { title: `${title} | ${SITE_NAME}`, description, url: `${SITE_URL}${canonical}`, type: 'article', siteName: SITE_NAME }, twitter: { card: 'summary_large_image', title, description } };49}5051export default async function FamilyPage({ params }: Params) {52  const { slug } = await params;53  const f = await load(slug);54  if (f.slug && f.slug !== slug) permanentRedirect(routes.family(f.slug));55  const bench = await safe(apiD1.benchmarks());56  const benchNames: Record<string, string> = {};57  for (const b of bench?.items ?? []) benchNames[b.slug] = b.name;58  const canonical = routes.family(f.slug);59  const members = [...f.members].sort((a, b) => String(b.key_facts?.release_date ?? '').localeCompare(String(a.key_facts?.release_date ?? '')) || a.model.name.localeCompare(b.model.name));60  const pmin = num(f.param_range?.min);61  const pmax = num(f.param_range?.max);62  const nodes = f.members.map((m) => ({ id: m.model.id, label: m.model.name, href: routes.entity(m.model), sub: num(m.model.attributes?.parameter_count) !== null ? fmtParams(m.model.attributes.parameter_count) : undefined }));63  const licTotal = f.licenses.reduce((n, l) => n + (num(l.models) ?? 0), 0);64  const ranks = Object.entries(f.benchmark_best ?? {}).sort((a, b) => a[1].rank - b[1].rank);65  const root = typeof f.summary?.attributes?.family_root === 'string' ? (f.summary.attributes.family_root as string) : null;66  const ld = { '@context': 'https://schema.org', '@type': 'CreativeWorkSeries', name: f.name, url: `${SITE_URL}${canonical}`, description: describe(f), creator: f.organization ? { '@type': 'Organization', name: f.organization.name } : undefined, hasPart: members.slice(0, 20).map((m) => ({ '@type': 'SoftwareApplication', name: m.model.name, url: `${SITE_URL}${routes.entity(m.model)}` })) };6768  return (69    <Container wide>70      <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }} />71      <ViewBeacon path={canonical} />72      <nav aria-label="Breadcrumb" className="pt-5 text-xs text-ink-3">73        <ol className="flex flex-wrap items-center gap-1.5">74          <li>75            <Link href="/" className="hover:text-ink">76              AI Atlas77            </Link>78          </li>79          <li aria-hidden>/</li>80          <li>81            <Link href={routes.families()} className="hover:text-ink">82              Families83            </Link>84          </li>85          {f.organization && (86            <>87              <li aria-hidden>/</li>88              <li>89                <Link href={routes.entity({ entity_type: 'company', slug: f.organization.slug })} className="hover:text-ink">90                  {f.organization.name}91                </Link>92              </li>93            </>94          )}95          <li aria-hidden>/</li>96          <li className="text-ink-2">{f.name}</li>97        </ol>98      </nav>99      <header className="pb-6 pt-4 md:pt-5" data-family-header>100        <div className="flex flex-wrap items-center gap-2">101          <EntityBadge type="model_family" />102          {!f.canonical && (103            <span className="inline-flex items-center gap-0.5 rounded-[3px] bg-warning-soft px-1.5 text-[11px] font-medium text-warning">104              label-based family105              <Hint text={f.note ?? 'Grouped by the family label stated by sources; not yet a canonical model_family entity. Counts may change when canonicalised.'} />106            </span>107          )}108          {root && root !== f.name && <span className="text-xs text-ink-3">root · {root}</span>}109        </div>110        <div className="mt-3 flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">111          <div className="min-w-0">112            <h1 className="display text-[30px] md:text-[44px]">{f.name}</h1>113            <p className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-[15px] text-ink-2">114              {f.organization && (115                <Link href={routes.entity({ entity_type: 'company', slug: f.organization.slug })} className="font-medium text-ink hover:text-accent">116                  {f.organization.name}117                </Link>118              )}119              <Link href={`/models?family=${encodeURIComponent(f.slug)}`} className="link">120                All {fmtInt(f.model_count)} models in the terminal →121              </Link>122            </p>123            {f.summary?.description && <p className="mt-3 max-w-3xl text-[15px] leading-relaxed text-ink-2">{f.summary.description}</p>}124          </div>125          {f.summary && (126            <div className="shrink-0 text-xs text-ink-3 lg:text-right">127              <QualityMark q={f.summary.quality?.score} label />128              <p className="mt-1">first seen {fmtDate(f.summary.first_seen_at)}</p>129              <p className="mono mt-0.5 text-[11px]">{f.id}</p>130            </div>131          )}132        </div>133        <dl className="tnum mt-4 grid grid-cols-2 gap-x-6 gap-y-3 border-y border-rule py-3 sm:grid-cols-3 lg:grid-cols-6" data-family-strip>134          {[135            ['Models', fmtInt(f.model_count)],136            ['First release', f.first_release ? fmtDate(f.first_release) : '—'],137            ['Latest release', f.last_release ? fmtDate(f.last_release) : '—'],138            ['Parameters', pmin === null ? '—' : pmin === pmax ? fmtParams(pmin) : `${fmtParams(pmin)} – ${fmtParams(pmax)}`],139            ['Artifacts', fmtInt(f.artifacts_count)],140            ['Providers', fmtInt(f.providers?.length ?? 0)],141          ].map(([k, v]) => (142            <div key={k} className="min-w-0">143              <dt className="eyebrow">{k}</dt>144              <dd className="mt-0.5 truncate text-[15px] font-medium text-ink">{v}</dd>145            </div>146          ))}147        </dl>148      </header>149150      <div className="space-y-10 pb-16">151        <section id="releases">152          <p className="eyebrow mb-2">Releases</p>153          <FamilyReleaseLanes members={f.members} />154        </section>155156        <section id="members">157          <p className="eyebrow mb-2">158            Members <span className="tnum text-ink-3">{fmtInt(members.length)}</span>159          </p>160          {members.length === 0 ? (161            <EmptyState title="No member recorded" />162          ) : (163            <ScrollX><DataTable caption="Family members" compact>164              <thead>165                <tr>166                  <Th>Model</Th>167                  <Th num>Params</Th>168                  <Th num>Context</Th>169                  <Th>Openness</Th>170                  <Th>Licence</Th>171                  <Th>Released</Th>172                  <Th>Status</Th>173                  <Th>Best rank</Th>174                  <Th className="text-right">175                    <span className="sr-only">Compare</span>176                  </Th>177                </tr>178              </thead>179              <tbody>180                {members.map((m) => {181                  const a = m.model.attributes ?? {};182                  const kf = m.key_facts ?? {};183                  const best = Object.entries(m.benchmark_ranks ?? {}).sort((x, y) => x[1] - y[1])[0];184                  const status = String(kf.status ?? m.model.status ?? '');185                  return (186                    <tr key={m.model.id}>187                      <Td primary>188                        <EntityLink e={m.model} />189                        {m.model.organization && m.model.organization.slug !== f.organization?.slug && <span className="block text-[11px] text-ink-3">{m.model.organization.name}</span>}190                      </Td>191                      <Td num label="Params" className="tnum">192                        {num(a.parameter_count) === null ? <span className="text-ink-3">—</span> : fmtParams(a.parameter_count)}193                        {num(a.active_parameter_count) !== null && num(a.active_parameter_count) !== num(a.parameter_count) && <span className="block text-[11px] text-ink-3">{fmtParams(a.active_parameter_count)} active</span>}194                      </Td>195                      <Td num label="Context" className="tnum">196                        {num(kf.context_length ?? a.context_length) === null ? <span className="text-ink-3">—</span> : fmtTokens(kf.context_length ?? a.context_length)}197                      </Td>198                      <Td label="Openness">{typeof (kf.openness ?? a.openness) === 'string' ? <OpennessChip openness={String(kf.openness ?? a.openness)} /> : <span className="text-ink-3">—</span>}</Td>199                      <Td label="Licence" className="max-w-[10rem] truncate text-xs text-ink-2">200                        {typeof (a.license_key ?? a.license) === 'string' ? <Link href={`/licenses/${encodeURIComponent(String(a.license_key ?? a.license))}`} className="hover:text-accent">{String(a.license_key ?? a.license)}</Link> : <span className="text-ink-3">—</span>}201                      </Td>202                      <Td label="Released" className="tnum whitespace-nowrap text-ink-2">203                        {typeof kf.release_date === 'string' ? fmtDate(kf.release_date) : <span className="text-ink-3">—</span>}204                      </Td>205                      <Td label="Status">206                        <StatusBadge status={status} />207                      </Td>208                      <Td label="Best rank" className="text-xs text-ink-2">209                        {best ? (210                          <Link href={routes.benchmark(best[0])} className="whitespace-nowrap hover:text-accent" title={benchNames[best[0]] ?? best[0]}>211                            <span className="tnum font-medium text-ink">#{fmtInt(best[1])}</span> {(benchNames[best[0]] ?? best[0]).replace('Artificial Analysis ', 'AA ').slice(0, 22)}212                          </Link>213                        ) : (214                          <span className="text-ink-3">—</span>215                        )}216                      </Td>217                      <Td className="text-right">218                        <CompareButton e={m.model} size="sm" label="" />219                      </Td>220                    </tr>221                  );222                })}223              </tbody>224            </DataTable></ScrollX>225          )}226        </section>227228        <div className="grid gap-10 lg:grid-cols-[minmax(0,1fr)_22rem]">229          <section id="lineage" className="min-w-0">230            <div className="mb-2 flex items-baseline justify-between gap-2">231              <p className="eyebrow">232                Lineage among members <span className="tnum text-ink-3">{fmtInt(f.lineage.length)} relations</span>233              </p>234              {f.summary && (235                <Link href={`${routes.graph(f.summary.slug)}?mode=lineage`} className="link text-xs">236                  Open in Graph →237                </Link>238              )}239            </div>240            <MiniGraph nodes={nodes} edges={f.lineage} title={`Lineage of the ${f.name} family`} />241          </section>242          <aside className="min-w-0 space-y-8">243            <section id="licences">244              <p className="eyebrow mb-2">Licence mix</p>245              {f.licenses.length ? (246                <ul className="space-y-1.5 text-sm">247                  {f.licenses.map((l) => (248                    <li key={l.key} className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3">249                      <Link href={`/licenses/${encodeURIComponent(l.key)}`} className="truncate text-ink hover:text-accent" title={l.label}>250                        {l.label}251                      </Link>252                      <span className="tnum text-xs text-ink-2">{fmtInt(l.models)}</span>253                      <span className="col-span-2 h-1 rounded-sm bg-surface-2">254                        <span className="block h-full rounded-sm bg-accent" style={{ width: `${licTotal ? (100 * (num(l.models) ?? 0)) / licTotal : 0}%` }} />255                      </span>256                    </li>257                  ))}258                </ul>259              ) : (260                <p className="text-sm text-ink-3">No licence recorded on the members.</p>261              )}262            </section>263            <section id="providers">264              <p className="eyebrow mb-2">265                Providers <span className="tnum text-ink-3">{fmtInt(f.providers?.length ?? 0)}</span>266              </p>267              {f.providers?.length ? (268                <p className="flex flex-wrap gap-x-3 gap-y-1 text-sm">269                  {f.providers.map((p) => (270                    <EntityLink key={p.id} e={p} />271                  ))}272                </p>273              ) : (274                <p className="text-sm text-ink-3">No provider deployment recorded.</p>275              )}276            </section>277            <section id="modalities">278              <p className="eyebrow mb-2">Modalities</p>279              <p className="flex flex-wrap gap-1">{f.modalities?.length ? f.modalities.map((m) => <Chip key={m}>{m}</Chip>) : <span className="text-sm text-ink-3">—</span>}</p>280            </section>281            {ranks.length > 0 && (282              <section id="best-ranks">283                <p className="eyebrow mb-2">Best benchmark ranks</p>284                <ul className="divide-y divide-rule border-y border-rule text-sm">285                  {ranks.slice(0, 8).map(([slug, r]) => (286                    <li key={slug} className="grid grid-cols-[auto_minmax(0,1fr)] items-baseline gap-x-3 py-1.5">287                      <span className="tnum font-medium">#{fmtInt(r.rank)}</span>288                      <span className="min-w-0 truncate">289                        <Link href={routes.benchmark(slug)} className="text-ink hover:text-accent">290                          {benchNames[slug] ?? slug}291                        </Link>292                        <span className="block truncate text-[11px] text-ink-3">{r.model}</span>293                      </span>294                    </li>295                  ))}296                </ul>297              </section>298            )}299          </aside>300        </div>301302        <section id="progress">303          <p className="eyebrow mb-2">Benchmark progress</p>304          <FamilyBenchmarkProgress members={f.members} benchNames={benchNames} />305        </section>306        <Note>307          Aggregates are computed from the members' sourced claims; members without a release date or parameter count are left out of the range and timeline rather than guessed. <Link href="/methodology" className="link">Methodology →</Link>308        </Note>309      </div>310      <CompareTrayBar />311    </Container>312  );313}314